﻿/*
 * GlassPanelForm - A transparent form that is placed over an FluentListView
 * to allow flicker-free overlay images during scrolling.
 *
 * Author: Phillip Piper
 * Date: 14/04/2009 4:36 PM
 *
 * Change log:
 * 2010-08-18   JPP  - Added WS_EX_TOOLWINDOW style so that the form won't appear in Alt-Tab list.
 * v2.4
 * 2010-03-11   JPP  - Work correctly in MDI applications -- more or less. Actually, less than more.
 *                     They don't crash but they don't correctly handle overlapping MDI children.
 *                     Overlays from one control are shown on top of other other windows.
 * 2010-03-09   JPP  - Correctly Unbind() when the related FluentListView is disposed.
 * 2009-10-28   JPP  - Use FindForm() rather than TopMostControl, since the latter doesn't work
 *                     as I expected when the OLV is part of an MDI child window. Thanks to
 *                     wvd_vegt who tracked this down.
 * v2.3
 * 2009-08-19   JPP  - Only hide the glass pane on resize, not on move
 *                   - Each glass panel now only draws one overlays
 * v2.2
 * 2009-06-05   JPP  - Handle when owning window is a topmost window
 * 2009-04-14   JPP  - Initial version
 *
 * To do:
 * 
 * Copyright (C) 2009-2014 Phillip Piper
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
 */

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using Fluent.Lists;

namespace Fluent {
	/// <summary>
	/// A GlassPanelForm sits transparently over an FluentListView to show overlays.
	/// </summary>
	internal partial class GlassPanelForm : Form {
		public GlassPanelForm() {
			Name = "GlassPanelForm";
			Text = "GlassPanelForm";

			ClientSize = new Size(0, 0);
			ControlBox = false;
			FormBorderStyle = FormBorderStyle.None;
			SizeGripStyle = SizeGripStyle.Hide;
			StartPosition = FormStartPosition.Manual;
			MaximizeBox = false;
			MinimizeBox = false;
			ShowIcon = false;
			ShowInTaskbar = false;
			FormBorderStyle = FormBorderStyle.None;

			SetStyle(ControlStyles.Selectable, false);

			Opacity = 0.5f;
			BackColor = Color.FromArgb(255, 254, 254, 254);
			TransparencyKey = BackColor;
			HideGlass();
			NativeMethods.ShowWithoutActivate(this);
		}

		protected override void Dispose(bool disposing) {
			if (disposing) {
				Unbind();
			}

			base.Dispose(disposing);
		}

		#region Properties

		/// <summary>
		/// Get the low-level windows flag that will be given to CreateWindow.
		/// </summary>
		protected override CreateParams CreateParams {
			get {
				var cp = base.CreateParams;
				cp.ExStyle |= 0x20; // WS_EX_TRANSPARENT
				cp.ExStyle |= 0x80; // WS_EX_TOOLWINDOW 
				return cp;
			}
		}

		#endregion

		#region Commands

		/// <summary>
		/// Attach this form to the given FluentListView
		/// </summary>        
		public void Bind(AdvancedListView olv, IOverlay overlay) {
			if (objectListView != null) {
				Unbind();
			}

			objectListView = olv;
			Overlay = overlay;
			mdiClient = null;
			mdiOwner = null;

			if (objectListView == null) {
				return;
			}

			// NOTE: If you listen to any events here, you *must* stop listening in Unbind()

			objectListView.Disposed += new EventHandler(objectListView_Disposed);
			objectListView.LocationChanged += new EventHandler(objectListView_LocationChanged);
			objectListView.SizeChanged += new EventHandler(objectListView_SizeChanged);
			objectListView.VisibleChanged += new EventHandler(objectListView_VisibleChanged);
			objectListView.ParentChanged += new EventHandler(objectListView_ParentChanged);

			// Collect our ancestors in the widget hierachy
			if (ancestors == null) {
				ancestors = new List<Control>();
			}

			var parent = objectListView.Parent;
			while (parent != null) {
				ancestors.Add(parent);
				parent = parent.Parent;
			}

			// Listen for changes in the hierachy
			foreach (var ancestor in ancestors) {
				ancestor.ParentChanged += new EventHandler(objectListView_ParentChanged);
				var tabControl = ancestor as TabControl;
				if (tabControl != null) {
					tabControl.Selected += new TabControlEventHandler(tabControl_Selected);
				}
			}

			// Listen for changes in our owning form
			Owner = objectListView.FindForm();
			myOwner = Owner;
			if (Owner != null) {
				Owner.LocationChanged += new EventHandler(Owner_LocationChanged);
				Owner.SizeChanged += new EventHandler(Owner_SizeChanged);
				Owner.ResizeBegin += new EventHandler(Owner_ResizeBegin);
				Owner.ResizeEnd += new EventHandler(Owner_ResizeEnd);
				if (Owner.TopMost) {
					// We can't do this.TopMost = true; since that will activate the panel,
					// taking focus away from the owner of the listview
					NativeMethods.MakeTopMost(this);
				}

				// We need special code to handle MDI
				mdiOwner = Owner.MdiParent;
				if (mdiOwner != null) {
					mdiOwner.LocationChanged += new EventHandler(Owner_LocationChanged);
					mdiOwner.SizeChanged += new EventHandler(Owner_SizeChanged);
					mdiOwner.ResizeBegin += new EventHandler(Owner_ResizeBegin);
					mdiOwner.ResizeEnd += new EventHandler(Owner_ResizeEnd);

					// Find the MDIClient control, which houses all MDI children
					foreach (Control c in mdiOwner.Controls) {
						mdiClient = c as MdiClient;
						if (mdiClient != null) {
							break;
						}
					}

					if (mdiClient != null) {
						mdiClient.ClientSizeChanged += new EventHandler(myMdiClient_ClientSizeChanged);
					}
				}
			}

			UpdateTransparency();
		}

		private void myMdiClient_ClientSizeChanged(object sender, EventArgs e) {
			RecalculateBounds();
			Invalidate();
		}

		/// <summary>
		/// Made the overlay panel invisible
		/// </summary>
		public void HideGlass() {
			if (!isGlassShown) {
				return;
			}

			isGlassShown = false;
			Bounds = new Rectangle(-10000, -10000, 1, 1);
		}

		/// <summary>
		/// Show the overlay panel in its correctly location
		/// </summary>
		/// <remarks>
		/// If the panel is always shown, this method does nothing.
		/// If the panel is being resized, this method also does nothing.
		/// </remarks>
		public void ShowGlass() {
			if (isGlassShown || isDuringResizeSequence) {
				return;
			}

			isGlassShown = true;
			RecalculateBounds();
		}

		/// <summary>
		/// Detach this glass panel from its previous FluentListView
		/// </summary>        
		/// <remarks>
		/// You should unbind the overlay panel before making any changes to the 
		/// widget hierarchy.
		/// </remarks>
		public void Unbind() {
			if (objectListView != null) {
				objectListView.Disposed -= new EventHandler(objectListView_Disposed);
				objectListView.LocationChanged -= new EventHandler(objectListView_LocationChanged);
				objectListView.SizeChanged -= new EventHandler(objectListView_SizeChanged);
				objectListView.VisibleChanged -= new EventHandler(objectListView_VisibleChanged);
				objectListView.ParentChanged -= new EventHandler(objectListView_ParentChanged);
				objectListView = null;
			}

			if (ancestors != null) {
				foreach (var parent in ancestors) {
					parent.ParentChanged -= new EventHandler(objectListView_ParentChanged);
					var tabControl = parent as TabControl;
					if (tabControl != null) {
						tabControl.Selected -= new TabControlEventHandler(tabControl_Selected);
					}
				}

				ancestors = null;
			}

			if (myOwner != null) {
				myOwner.LocationChanged -= new EventHandler(Owner_LocationChanged);
				myOwner.SizeChanged -= new EventHandler(Owner_SizeChanged);
				myOwner.ResizeBegin -= new EventHandler(Owner_ResizeBegin);
				myOwner.ResizeEnd -= new EventHandler(Owner_ResizeEnd);
				myOwner = null;
			}

			if (mdiOwner != null) {
				mdiOwner.LocationChanged -= new EventHandler(Owner_LocationChanged);
				mdiOwner.SizeChanged -= new EventHandler(Owner_SizeChanged);
				mdiOwner.ResizeBegin -= new EventHandler(Owner_ResizeBegin);
				mdiOwner.ResizeEnd -= new EventHandler(Owner_ResizeEnd);
				mdiOwner = null;
			}

			if (mdiClient != null) {
				mdiClient.ClientSizeChanged -= new EventHandler(myMdiClient_ClientSizeChanged);
				mdiClient = null;
			}
		}

		#endregion

		#region Event Handlers

		private void objectListView_Disposed(object sender, EventArgs e) {
			Unbind();
		}

		/// <summary>
		/// Handle when the form that owns the FluentListView begins to be resized
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void Owner_ResizeBegin(object sender, EventArgs e) {
			// When the top level window is being resized, we just want to hide
			// the overlay window. When the resizing finishes, we want to show
			// the overlay window, if it was shown before the resize started.
			isDuringResizeSequence = true;
			wasGlassShownBeforeResize = isGlassShown;
		}

		/// <summary>
		/// Handle when the form that owns the FluentListView finished to be resized
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void Owner_ResizeEnd(object sender, EventArgs e) {
			isDuringResizeSequence = false;
			if (wasGlassShownBeforeResize) {
				ShowGlass();
			}
		}

		/// <summary>
		/// The owning form has moved. Move the overlay panel too.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void Owner_LocationChanged(object sender, EventArgs e) {
			if (mdiOwner != null) {
				HideGlass();
			}
			else {
				RecalculateBounds();
			}
		}

		/// <summary>
		/// The owning form is resizing. Hide our overlay panel until the resizing stops
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void Owner_SizeChanged(object sender, EventArgs e) {
			HideGlass();
		}


		/// <summary>
		/// Handle when the bound OLV changes its location. The overlay panel must 
		/// be moved too, IFF it is currently visible.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void objectListView_LocationChanged(object sender, EventArgs e) {
			if (isGlassShown) {
				RecalculateBounds();
			}
		}

		/// <summary>
		/// Handle when the bound OLV changes size. The overlay panel must 
		/// resize too, IFF it is currently visible.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void objectListView_SizeChanged(object sender, EventArgs e) {
			// This event is triggered in all sorts of places, and not always when the size changes.
			//if (this.isGlassShown) {
			//    this.Size = this.objectListView.ClientSize;
			//}
		}

		/// <summary>
		/// Handle when the bound OLV is part of a TabControl and that
		/// TabControl changes tabs. The overlay panel is hidden. The
		/// first time the bound OLV is redrawn, the overlay panel will
		/// be shown again.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void tabControl_Selected(object sender, TabControlEventArgs e) {
			HideGlass();
		}

		/// <summary>
		/// Somewhere the parent of the bound OLV has changed. Update
		/// our events.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void objectListView_ParentChanged(object sender, EventArgs e) {
			var olv = objectListView;
			var overlay = Overlay;
			Unbind();
			Bind(olv, overlay);
		}

		/// <summary>
		/// Handle when the bound OLV changes its visibility.
		/// The overlay panel should match the OLV's visibility.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void objectListView_VisibleChanged(object sender, EventArgs e) {
			if (objectListView.Visible) {
				ShowGlass();
			}
			else {
				HideGlass();
			}
		}

		#endregion

		#region Implementation

		protected override void OnPaint(PaintEventArgs e) {
			if (objectListView == null || Overlay == null) {
				return;
			}

			var g = e.Graphics;
			g.TextRenderingHint = AdvancedListView.TextRenderingHint;
			g.SmoothingMode = AdvancedListView.SmoothingMode;

			//g.DrawRectangle(new Pen(Color.Green, 4.0f), this.ClientRectangle);

			// If we are part of an MDI app, make sure we don't draw outside the bounds
			if (mdiClient != null) {
				var r = mdiClient.RectangleToScreen(mdiClient.ClientRectangle);
				var r2 = objectListView.RectangleToClient(r);
				g.SetClip(r2, System.Drawing.Drawing2D.CombineMode.Intersect);
			}

			Overlay.Draw(objectListView, g, objectListView.ClientRectangle);
		}

		protected void RecalculateBounds() {
			if (!isGlassShown) {
				return;
			}

			var rect = objectListView.ClientRectangle;
			rect.X = 0;
			rect.Y = 0;
			Bounds = objectListView.RectangleToScreen(rect);
		}

		internal void UpdateTransparency() {
			var transparentOverlay = Overlay as ITransparentOverlay;
			if (transparentOverlay == null) {
				Opacity = objectListView.OverlayTransparency / 255.0f;
			}
			else {
				Opacity = transparentOverlay.Transparency / 255.0f;
			}
		}

		protected override void WndProc(ref Message m) {
			const int WM_NCHITTEST = 132;
			const int HTTRANSPARENT = -1;
			switch (m.Msg) {
				// Ignore all mouse interactions
				case WM_NCHITTEST:
					m.Result = (IntPtr) HTTRANSPARENT;
					break;
			}

			base.WndProc(ref m);
		}

		#endregion

		#region Implementation variables

		internal IOverlay Overlay;

		#endregion

		#region Private variables

		private AdvancedListView objectListView;
		private bool isDuringResizeSequence;
		private bool isGlassShown;
		private bool wasGlassShownBeforeResize;

		// Cache these so we can unsubscribe from events even when the OLV has been disposed.
		private Form myOwner;
		private Form mdiOwner;
		private List<Control> ancestors;
		private MdiClient mdiClient;

		#endregion
	}
}